package xyz.scootaloo.kami.cloud.service.impl

import io.vertx.core.Future
import xyz.scootaloo.kami.cloud.lang.*
import xyz.scootaloo.kami.cloud.model.po.CacheEntry
import xyz.scootaloo.kami.cloud.model.po.CacheUpdateExpiry
import xyz.scootaloo.kami.cloud.service.CacheService
import xyz.scootaloo.kami.cloud.service.CacheService.Companion.INVALID_EX_TIME
import xyz.scootaloo.kami.cloud.service.CrontabService
import xyz.scootaloo.kami.cloud.util.toUnit
import java.util.TreeMap

/**
 * @author flutterdash@qq.com
 * @since 2022/4/10 21:38
 */
object CacheServiceImpl : CacheService {

    override fun crontab(): CrontabService.Crontab {
        return AutoGCCacheCrontab
    }

    override fun put(key: String, value: String, ex: Long): Future<Unit> {
        val entry = serializer(CacheEntry(key, value, ex))
        return Bus.request(Constant.EB_CACHE_PUT, entry).toUnit()
    }

    override fun get(key: String): Future<String?> {
        return Bus.request(Constant.EB_CACHE_GET, key)
    }

    override fun delete(key: String): Future<Unit> {
        return Bus.request(Constant.EB_CACHE_DEL, key).toUnit()
    }

    override fun updateExpiryTime(key: String, newExpiryTime: Long): Future<Unit> {
        val expiryInfo = serializer(CacheUpdateExpiry(key, newExpiryTime))
        return Bus.request(Constant.EB_CACHE_UpEx, expiryInfo).toUnit()
    }

    override fun internal(): CacheService.InternalCacheServiceApi {
        return InternalCacheServiceApiImpl
    }

    // implements

    private val log by lazy { getLogger("cache-service") }

    private val kvMap = HashMap<String, CacheItem>()
    private val expiryMap = TreeMap<Long, String>()

    private fun putKeyValueWithExpiryTime(key: String, value: Any, expiryTime: Long) {
        if (expiryTime <= INVALID_EX_TIME) {
            log.warn("存储永久键: $key")
            kvMap[key] = CacheItem(value, INVALID_EX_TIME)
            return
        }

        val realExpiryTime = placeExpiryKeyWithoutRepetition(key, (currentTimeMillis() + expiryTime))
        kvMap[key] = CacheItem(value, realExpiryTime)
    }

    private fun placeExpiryKeyWithoutRepetition(cacheKey: String, expiryTime: Long): Long {
        if (!expiryMap.containsKey(expiryTime)) {
            expiryMap[expiryTime] = cacheKey
            return expiryTime
        }

        var validExpiryTimePst = expiryTime - 1
        var validExpiryTimeFut = expiryTime + 1
        while (true) {
            if (!expiryMap.containsKey(validExpiryTimePst)) {
                expiryMap[validExpiryTimePst] = cacheKey
                return validExpiryTimePst
            } else {
                validExpiryTimePst--
            }

            if (!expiryMap.containsKey(validExpiryTimeFut)) {
                expiryMap[validExpiryTimeFut] = cacheKey
                return validExpiryTimeFut
            } else {
                validExpiryTimeFut++
            }
        }
    }

    private fun getCache(key: String): Any? {
        val cacheItem = kvMap[key]
        return cacheItem?.value
    }

    private fun deleteCache(key: String) {
        val cacheItem = kvMap[key] ?: return
        val keyExpiryTime = cacheItem.expiryTime
        expiryMap.remove(keyExpiryTime)
        kvMap.remove(key)
    }

    private fun updateKeyValueExpiryTime(key: String, newExpiryTime: Long) {
        val cacheItem = kvMap[key] ?: return
        deleteCache(key)
        putKeyValueWithExpiryTime(key, cacheItem.value, newExpiryTime)
    }

    data class CacheItem(
        val value: Any,
        val expiryTime: Long
    )

    object InternalCacheServiceApiImpl : CacheService.InternalCacheServiceApi {
        override fun put(key: String, value: Any, ex: Long) {
            putKeyValueWithExpiryTime(key, value, ex)
        }

        override fun <T> get(key: String): T? {
            @Suppress("UNCHECKED_CAST")
            return getCache(key) as T?
        }

        override fun delete(key: String) {
            deleteCache(key)
        }

        override fun updateExpiryTime(key: String, newExpiryTime: Long) {
            updateKeyValueExpiryTime(key, newExpiryTime)
        }
    }

    object AutoGCCacheCrontab : CrontabService.Crontab {

        override var valid = true
        override val name = "cache-service"
        override var delay = CrontabService.Crontab.defDelay
        override val order = CrontabService.Crontab.defOrder

        override fun run(currentTimeMillis: Long) {
            if (kvMap.isEmpty() || expiryMap.isEmpty())
                return

            val invalidKeys = ArrayList<String>()
            for ((expiryTime, cacheKey) in expiryMap) {
                if (expiryTime > currentTimeMillis)
                    break
                invalidKeys.add(cacheKey)
            }

            if (invalidKeys.isNotEmpty()) {
                deleteKeys(invalidKeys)
            }
        }

        private fun deleteKeys(keys: List<String>) {
            keys.forEach(::deleteCache)
        }
    }
}